﻿using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace MM.Web.Helpers
{
    /// <summary>
    /// Helper for Updating objects
    /// </summary>
    public static class ObjectHelper
    {
        /// <summary>
        /// Updates the current object from the source object.
        /// </summary>
        /// <typeparam name="T">Type of the current object.</typeparam>
        /// <typeparam name="T1">Type of the source object.</typeparam>
        /// <param name="currentObject">The current object.</param>
        /// <param name="sourceObject">The source object.</param>
        /// <param name="isAllProps">Is all properties from source object to update.</param>
        /// <param name="propertiesToUpdate">The properties from source object to update.</param>
        /// <param name="propertiesToIgnore">The properties from source object to ignore.</param>
        /// <param name="propertiesToRename">The properties from source object to rename (propertyNameSource, propertyNameCurrent) </param>
        public static void UpdateObject<T, T1>(this T1 currentObject, T sourceObject, bool? isAllProps = true
            , IEnumerable<string> propertiesToUpdate = null
            , IEnumerable<string> propertiesToIgnore = null
            , Dictionary<string, string> propertiesToRename = null)
        {
            var properties = PreparePropertiesToUpdate<T, T1>(isAllProps, propertiesToUpdate, propertiesToIgnore, propertiesToRename);

            //update entity from DTO
            foreach (var prop in properties)
            {
                var propertyInfoSource = GetPropertyInfo<T>(prop.Key);
                var propertyInfoCurrent = GetPropertyInfo<T1>(prop.Value);

                if (propertyInfoSource == null || propertyInfoCurrent == null) continue;

                propertyInfoCurrent.SetValue(currentObject, propertyInfoSource.GetValue(sourceObject, null), null);
            }
        }

        private static PropertyInfo GetPropertyInfo<T>(string propName)
        {
            return typeof(T).GetProperties().FirstOrDefault(_ => _.Name == propName);
        }


        /// <summary>
        /// Get all Entity properties , else get only properties from propertiesToUpdate list
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <typeparam name="T1">The type of the 1.</typeparam>
        /// <param name="isAllProps">The is all props.</param>
        /// <param name="propertiesToUpdate">The properties to update.</param>
        /// <param name="propIgnoreList">The prop ignore list.</param>
        /// <param name="propertiesToRename">The properties to rename.</param>
        /// <returns></returns>
        private static Dictionary<string, string> PreparePropertiesToUpdate<T, T1>(bool? isAllProps
            , IEnumerable<string> propertiesToUpdate
            , IEnumerable<string> propIgnoreList
            , Dictionary<string, string> propertiesToRename = null)
        {
            Dictionary<string, string> properties;

            if ((isAllProps == null || (bool)isAllProps) && propertiesToUpdate == null)
            {
                var allPropsSource = typeof(T).GetProperties();

                properties = propertiesToRename == null
                                         ? allPropsSource.ToDictionary(key => key.Name, arg => arg.Name)
                                         : propertiesToRename.GetAllPropsFromSourceAndReplaceDifferent(allPropsSource.Select(_ => _.Name));
            }
            else
            {
                if (propertiesToUpdate == null)
                {
                    return null;
                }

                properties = propertiesToRename == null
                                 ? propertiesToUpdate.ToDictionary(key => key, arg => arg)
                                 : propertiesToRename.GetAllPropsFromSourceAndReplaceDifferent(propertiesToUpdate);
            }

            if (propIgnoreList != null)
            {
                properties = properties.GetPropsWithoutPropsToRemove(propIgnoreList);
            }

            var allCurrentProps = typeof(T1).GetProperties();

            return properties.GetOnlySameProperties(allCurrentProps);
        }

        /// <summary>
        /// Recheck if all properties to update from DTO have appropriate properties from Entity
        /// </summary>
        /// <param name="dictionary">The dictionary.</param>
        /// <param name="propertyInfos">The property infos.</param>
        /// <returns></returns>
        private static Dictionary<string, string> GetOnlySameProperties(this Dictionary<string, string> dictionary, IEnumerable<PropertyInfo> propertyInfos)
        {
            return dictionary.Where(_ => propertyInfos.Select(p => p.Name).Contains(_.Value)).ToDictionary(key => key.Key, arg => arg.Value);
        }

        /// <summary>
        /// Remove from propertiesToUpdate properties whitch are in propertiesToIgnore
        /// </summary>
        /// <param name="dictionary">The dictionary.</param>
        /// <param name="propertyToRemove">The property to remove.</param>
        /// <returns></returns>
        private static Dictionary<string, string> GetPropsWithoutPropsToRemove(this Dictionary<string, string> dictionary, IEnumerable<string> propertyToRemove)
        {
            return dictionary.Where(_ => !propertyToRemove.Contains(_.Key)).ToDictionary(key => key.Key, arg => arg.Value);
        }

        /// <summary>
        ///  Get all properties from DTO and replace those of them which names are different of Entity properties names
        /// </summary>
        /// <param name="dictionary">The dictionary.</param>
        /// <param name="propertyInfos">The property infos.</param>
        /// <returns></returns>
        private static Dictionary<string, string> GetAllPropsFromSourceAndReplaceDifferent(this Dictionary<string, string> dictionary, IEnumerable<string> propertyInfos)
        {
            var listPropSource = dictionary.Select(p => p.Key);

            IEnumerable<string> list = propertyInfos as List<string> ?? propertyInfos.ToList();

            var propList = list.Where(_ => !listPropSource.Contains(_)).ToDictionary(key => key, arg => arg).ToList();

            propList.AddRange(list.Where(listPropSource.Contains).ToDictionary(key => key, arg => dictionary.FirstOrDefault(_ => _.Key == arg).Value));
            return propList.ToDictionary(key => key.Key, arg => arg.Value);
        }
    }
}